package com.frank.timber;

import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Collections.unmodifiableList;

/**
 * HiLogging for lazy people.
 */
@SuppressWarnings({"WeakerAccess", "unused"}) // Public API.
public final class Timber {
    /**
     * HiLog a verbose message with optional format args.
     */
    public static void v(@NonNls String message, Object... args) {
        TREE_OF_SOULS.v(message, args);
    }

    /**
     * HiLog a verbose exception and a message with optional format args.
     */
    public static void v(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.v(t, message, args);
    }

    /**
     * HiLog a verbose exception.
     */
    public static void v(Throwable t) {
        TREE_OF_SOULS.v(t);
    }

    /**
     * HiLog a debug message with optional format args.
     */
    public static void d(@NonNls String message, Object... args) {
        TREE_OF_SOULS.d(message, args);
    }

    /**
     * HiLog a debug exception and a message with optional format args.
     */
    public static void d(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.d(t, message, args);
    }

    /**
     * HiLog a debug exception.
     */
    public static void d(Throwable t) {
        TREE_OF_SOULS.d(t);
    }

    /**
     * HiLog an info message with optional format args.
     */
    public static void i(@NonNls String message, Object... args) {
        TREE_OF_SOULS.i(message, args);
    }

    /**
     * HiLog an info exception and a message with optional format args.
     */
    public static void i(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.i(t, message, args);
    }

    /**
     * HiLog an info exception.
     */
    public static void i(Throwable t) {
        TREE_OF_SOULS.i(t);
    }

    /**
     * HiLog a warning message with optional format args.
     */
    public static void w(@NonNls String message, Object... args) {
        TREE_OF_SOULS.w(message, args);
    }

    /**
     * HiLog a warning exception and a message with optional format args.
     */
    public static void w(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.w(t, message, args);
    }

    /**
     * HiLog a warning exception.
     */
    public static void w(Throwable t) {
        TREE_OF_SOULS.w(t);
    }

    /**
     * HiLog an error message with optional format args.
     */
    public static void e(@NonNls String message, Object... args) {
        TREE_OF_SOULS.e(message, args);
    }

    /**
     * HiLog an error exception and a message with optional format args.
     */
    public static void e(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.e(t, message, args);
    }

    /**
     * HiLog an error exception.
     */
    public static void e(Throwable t) {
        TREE_OF_SOULS.e(t);
    }

    /**
     * HiLog an FATAL message with optional format args.
     */
    public static void wtf(@NonNls String message, Object... args) {
        TREE_OF_SOULS.wtf(message, args);
    }

    /**
     * HiLog an FATAL exception and a message with optional format args.
     */
    public static void wtf(Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.wtf(t, message, args);
    }

    /**
     * HiLog an FATAL exception.
     */
    public static void wtf(Throwable t) {
        TREE_OF_SOULS.wtf(t);
    }

    /**
     * HiLog at {@code priority} a message with optional format args.
     */
    public static void HiLog(int priority, @NonNls String message, Object... args) {
        TREE_OF_SOULS.HiLog(priority, message, args);
    }

    /**
     * HiLog at {@code priority} an exception and a message with optional format args.
     */
    public static void HiLog(int priority, Throwable t, @NonNls String message, Object... args) {
        TREE_OF_SOULS.HiLog(priority, t, message, args);
    }

    /**
     * HiLog at {@code priority} an exception.
     */
    public static void HiLog(int priority, Throwable t) {
        TREE_OF_SOULS.HiLog(priority, t);
    }

    /**
     * A view into Timber's planted trees as a tree itself. This can be used for injecting a HiLogger
     * instance rather than using static methods or to facilitate testing.
     */
    @NotNull
    public static Tree asTree() {
        return TREE_OF_SOULS;
    }

    /**
     * Set a one-time tag for use on the next HiLogging call.
     */
    @NotNull
    public static Tree tag(String tag) {
        Tree[] forest = forestAsArray;
        for (Tree tree : forest) {
            tree.explicitTag.set(tag);
        }
        return TREE_OF_SOULS;
    }

    /**
     * Add a new HiLogging tree.
     */
    @SuppressWarnings("ConstantConditions") // Validating public API contract.
    public static void plant(@NotNull Tree tree) {
        if (tree == null) {
            throw new NullPointerException("tree == null");
        }
        if (tree == TREE_OF_SOULS) {
            throw new IllegalArgumentException("Cannot plant Timber into itself.");
        }
        synchronized (FOREST) {
            FOREST.add(tree);
            forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
        }
    }

    /**
     * Adds new HiLogging trees.
     */
    @SuppressWarnings("ConstantConditions") // Validating public API contract.
    public static void plant(@NotNull Tree... trees) {
        if (trees == null) {
            throw new NullPointerException("trees == null");
        }
        for (Tree tree : trees) {
            if (tree == null) {
                throw new NullPointerException("trees contains null");
            }
            if (tree == TREE_OF_SOULS) {
                throw new IllegalArgumentException("Cannot plant Timber into itself.");
            }
        }
        synchronized (FOREST) {
            Collections.addAll(FOREST, trees);
            forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
        }
    }

    /**
     * Remove a planted tree.
     */
    public static void uproot(@NotNull Tree tree) {
        synchronized (FOREST) {
            if (!FOREST.remove(tree)) {
                throw new IllegalArgumentException("Cannot uproot tree which is not planted: " + tree);
            }
            forestAsArray = FOREST.toArray(new Tree[FOREST.size()]);
        }
    }

    /**
     * Remove all planted trees.
     */
    public static void uprootAll() {
        synchronized (FOREST) {
            FOREST.clear();
            forestAsArray = TREE_ARRAY_EMPTY;
        }
    }

    /**
     * Return a copy of all planted {@linkplain Tree trees}.
     */
    @NotNull
    public static List<Tree> forest() {
        synchronized (FOREST) {
            return unmodifiableList(new ArrayList<>(FOREST));
        }
    }

    public static int treeCount() {
        synchronized (FOREST) {
            return FOREST.size();
        }
    }

    private static final Tree[] TREE_ARRAY_EMPTY = new Tree[0];
    // Both fields guarded by 'FOREST'.
    private static final List<Tree> FOREST = new ArrayList<>();
    static volatile Tree[] forestAsArray = TREE_ARRAY_EMPTY;

    /**
     * A {@link Tree} that delegates to all planted trees in the {@linkplain #FOREST forest}.
     */
    private static final Tree TREE_OF_SOULS = new Tree() {
        @Override
        public void v(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.v(message, args);
            }
        }

        @Override
        public void v(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.v(t, message, args);
            }
        }

        @Override
        public void v(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.v(t);
            }
        }

        @Override
        public void d(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.d(message, args);
            }
        }

        @Override
        public void d(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.d(t, message, args);
            }
        }

        @Override
        public void d(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.d(t);
            }
        }

        @Override
        public void i(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.i(message, args);
            }
        }

        @Override
        public void i(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.i(t, message, args);
            }
        }

        @Override
        public void i(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.i(t);
            }
        }

        @Override
        public void w(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.w(message, args);
            }
        }

        @Override
        public void w(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.w(t, message, args);
            }
        }

        @Override
        public void w(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.w(t);
            }
        }

        @Override
        public void e(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.e(message, args);
            }
        }

        @Override
        public void e(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.e(t, message, args);
            }
        }

        @Override
        public void e(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.e(t);
            }
        }

        @Override
        public void wtf(String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.wtf(message, args);
            }
        }

        @Override
        public void wtf(Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.wtf(t, message, args);
            }
        }

        @Override
        public void wtf(Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.wtf(t);
            }
        }

        @Override
        public void HiLog(int priority, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.HiLog(priority, message, args);
            }
        }

        @Override
        public void HiLog(int priority, Throwable t, String message, Object... args) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.HiLog(priority, t, message, args);
            }
        }

        @Override
        public void HiLog(int priority, Throwable t) {
            Tree[] forest = forestAsArray;
            for (Tree tree : forest) {
                tree.HiLog(priority, t);
            }
        }

        @Override
        protected void HiLog(int priority, String tag, @NotNull String message, Throwable t) {
            throw new AssertionError("Missing override for HiLog method.");
        }
    };

    private Timber() {
        throw new AssertionError("No instances.");
    }

    /**
     * A facade for handling HiLogging calls. Install instances via {@link #plant Timber.plant()}.
     */
    public static abstract class Tree {
        final ThreadLocal<String> explicitTag = new ThreadLocal<>();

        @Nullable
        String getTag() {
            String tag = explicitTag.get();
            if (tag != null) {
                explicitTag.remove();
            }
            return tag;
        }

        /**
         * HiLog a verbose message with optional format args.
         */
        public void v(String message, Object... args) {
            prepareHiLog(HiLog.LOG_APP, null, message, args);
        }

        /**
         * HiLog a verbose exception and a message with optional format args.
         */
        public void v(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.LOG_APP, t, message, args);
        }

        /**
         * HiLog a verbose exception.
         */
        public void v(Throwable t) {
            prepareHiLog(HiLog.LOG_APP, t, null);
        }

        /**
         * HiLog a debug message with optional format args.
         */
        public void d(String message, Object... args) {
            prepareHiLog(HiLog.DEBUG, null, message, args);
        }

        /**
         * HiLog a debug exception and a message with optional format args.
         */
        public void d(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.DEBUG, t, message, args);
        }

        /**
         * HiLog a debug exception.
         */
        public void d(Throwable t) {
            prepareHiLog(HiLog.DEBUG, t, null);
        }

        /**
         * HiLog an info message with optional format args.
         */
        public void i(String message, Object... args) {
            prepareHiLog(HiLog.INFO, null, message, args);
        }

        /**
         * HiLog an info exception and a message with optional format args.
         */
        public void i(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.INFO, t, message, args);
        }

        /**
         * HiLog an info exception.
         */
        public void i(Throwable t) {
            prepareHiLog(HiLog.INFO, t, null);
        }

        /**
         * HiLog a warning message with optional format args.
         */
        public void w(String message, Object... args) {
            prepareHiLog(HiLog.WARN, null, message, args);
        }

        /**
         * HiLog a warning exception and a message with optional format args.
         */
        public void w(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.WARN, t, message, args);
        }

        /**
         * HiLog a warning exception.
         */
        public void w(Throwable t) {
            prepareHiLog(HiLog.WARN, t, null);
        }

        /**
         * HiLog an error message with optional format args.
         */
        public void e(String message, Object... args) {
            prepareHiLog(HiLog.ERROR, null, message, args);
        }

        /**
         * HiLog an error exception and a message with optional format args.
         */
        public void e(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.ERROR, t, message, args);
        }

        /**
         * HiLog an error exception.
         */
        public void e(Throwable t) {
            prepareHiLog(HiLog.ERROR, t, null);
        }

        /**
         * HiLog an FATAL message with optional format args.
         */
        public void wtf(String message, Object... args) {
            prepareHiLog(HiLog.FATAL, null, message, args);
        }

        /**
         * HiLog an FATAL exception and a message with optional format args.
         */
        public void wtf(Throwable t, String message, Object... args) {
            prepareHiLog(HiLog.FATAL, t, message, args);
        }

        /**
         * HiLog an FATAL exception.
         */
        public void wtf(Throwable t) {
            prepareHiLog(HiLog.FATAL, t, null);
        }

        /**
         * HiLog at {@code priority} a message with optional format args.
         */
        public void HiLog(int priority, String message, Object... args) {
            prepareHiLog(priority, null, message, args);
        }

        /**
         * HiLog at {@code priority} an exception and a message with optional format args.
         */
        public void HiLog(int priority, Throwable t, String message, Object... args) {
            prepareHiLog(priority, t, message, args);
        }

        /**
         * HiLog at {@code priority} an exception.
         */
        public void HiLog(int priority, Throwable t) {
            prepareHiLog(priority, t, null);
        }

        /**
         * Return whether a message at {@code priority} should be HiLogged.
         *
         * @deprecated use {@link #isHiLoggable(String, int)} instead.
         */
        @Deprecated
        protected boolean isHiLoggable(int priority) {
            return true;
        }

        /**
         * Return whether a message at {@code priority} or {@code tag} should be HiLogged.
         */
        protected boolean isHiLoggable(@Nullable String tag, int priority) {
            //noinspection deprecation
            return isHiLoggable(priority);
        }

        private void prepareHiLog(int priority, Throwable t, String message, Object... args) {
            // Consume tag even when message is not HiLoggable so that next message is correctly tagged.
            String tag = getTag();

            if (!isHiLoggable(tag, priority)) {
                return;
            }
            if (message != null && message.length() == 0) {
                message = null;
            }
            if (message == null) {
                if (t == null) {
                    return; // Swallow message if it's null and there's no throwable.
                }
                message = getStackTraceString(t);
            } else {
                if (args != null && args.length > 0) {
                    message = formatMessage(message, args);
                }
                if (t != null) {
                    message += "\n" + getStackTraceString(t);
                }
            }

            HiLog(priority, tag, message, t);
        }

        /**
         * Formats a HiLog message with optional arguments.
         */
        protected String formatMessage(@NotNull String message, @NotNull Object[] args) {
            return String.format(message, args);
        }

        private String getStackTraceString(Throwable t) {
            // Don't replace this with HiLog.getStackTraceString() - it hides
            // UnknownHostException, which is not what we want.
            StringWriter sw = new StringWriter(256);
            PrintWriter pw = new PrintWriter(sw, false);
            t.printStackTrace(pw);
            pw.flush();
            return sw.toString();
        }

        /**
         * Write a HiLog message to its destination. Called for all level-specific methods by default.
         *
         * @param priority HiLog level. See {@link HiLog} for constants.
         * @param tag      Explicit or inferred tag. May be {@code null}.
         * @param message  Formatted HiLog message. May be {@code null}, but then {@code t} will not be.
         * @param t        Accompanying exceptions. May be {@code null}, but then {@code message} will not be.
         */
        protected abstract void HiLog(int priority, @Nullable String tag, @NotNull String message,
                                      @Nullable Throwable t);
    }

    /**
     * A {@link Tree Tree} for debug builds. Automatically infers the tag from the calling class.
     */
    public static class DebugTree extends Tree {
        private static final int MAX_HiLog_LENGTH = 4000;
        private static final int MAX_TAG_LENGTH = 23;
        private static final int CALL_STACK_INDEX = 5;
        private static final Pattern ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$");

        /**
         * Extract the tag which should be used for the message from the {@code element}. By default
         * this will use the class name without any anonymous class suffixes (e.g., {@code Foo$1}
         * becomes {@code Foo}).
         * <p>
         * Note: This will not be called if a {@linkplain #tag(String) manual tag} was specified.
         */
        @Nullable
        protected String createStackElementTag(@NotNull StackTraceElement element) {
            String tag = element.getClassName();
            Matcher m = ANONYMOUS_CLASS.matcher(tag);
            if (m.find()) {
                tag = m.replaceAll("");
            }
            tag = tag.substring(tag.lastIndexOf('.') + 1);
            // Tag length limit was removed in API 24.
            return tag;
        }

        @Override
        final String getTag() {
            String tag = super.getTag();
            if (tag != null) {
                return tag;
            }

            // DO NOT switch this to Thread.getCurrentThread().getStackTrace(). The test will pass
            // because Robolectric runs them on the JVM but on Android the elements are different.
            StackTraceElement[] stackTrace = new Throwable().getStackTrace();
            if (stackTrace.length <= CALL_STACK_INDEX) {
                throw new IllegalStateException(
                        "Synthetic stacktrace didn't have enough elements: are you using proguard?");
            }
            return createStackElementTag(stackTrace[CALL_STACK_INDEX]);
        }

        /**
         * Break up {@code message} into maximum-length chunks (if needed) and send to either
         * <p>
         * {@inheritDoc}
         */
        @Override
        protected void HiLog(int priority, String tag, @NotNull String message, Throwable t) {
            if (message.length() < MAX_HiLog_LENGTH) {
                if (priority == HiLog.FATAL) {
                    HiLog.fatal(new HiLogLabel(priority,priority,tag), message);
                } else {
                    println(priority, tag, message);
                }
                return;
            }

            // Split by line, then ensure each line can fit into HiLog's maximum length.
            for (int i = 0, length = message.length(); i < length; i++) {
                int newline = message.indexOf('\n', i);
                newline = newline != -1 ? newline : length;
                do {
                    int end = Math.min(newline, i + MAX_HiLog_LENGTH);
                    String part = message.substring(i, end);
                    if (priority == HiLog.FATAL) {
                        HiLog.fatal(new HiLogLabel(priority,priority,tag), part);
                    } else {
                        Timber.println(priority, tag, part);
                    }
                    i = end;
                } while (i < newline);
            }
        }
    }

    public static int println(int priority, String tag, String msg) {
        System.out.println(tag + ":" + msg);
        return 0;
    }
}
